﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using Newtonsoft.Json;
using SuperSocket.ClientEngine;
using WebSocket4Net;

namespace PusherClient
{
    public enum ConnectionState
    {
        Initialized,
        Connecting,
        Connected,
        Unavailable,
        Disconnected,
        Failed
    }

    internal class Connection : IConnection
    {
        private readonly WebSocket _websocket;
        private string _socketId;
        private readonly Timer _reconnectTimer;
        private bool _isDisposed;
        private object _sync  = new object();

        public ConnectionState State { get; private set; }

        public event EventHandler<EventArgs> Connected;
        public event EventHandler<ErrorEventArgs> Error;
        public event EventHandler<EventArgs> Unavailable;
        public event EventHandler<EventArgs> Disconnected;
        internal event EventHandler<MessageEventArgs> MessageReceived;
        internal event EventHandler<SubscribedEventArgs> Subscribed;

        public Connection(string endpoint)
        {
            this.State = ConnectionState.Initialized;

            this._reconnectTimer = new Timer(10000);
            this._reconnectTimer.Elapsed += ReconnectTimerElapsed;

            this._websocket = new WebSocket(endpoint);
            this._websocket.Error += websocket_Error;
            this._websocket.Closed += websocket_Closed;
            this._websocket.MessageReceived += websocket_MessageReceived;
            this._websocket.AutoSendPingInterval = 120;
            this._websocket.EnableAutoSendPing = true;

            Task.Factory.StartNew(Connect);
        }

        internal bool Send(PusherMessage message)
        {
            if (this.State == ConnectionState.Connected)
            {
                try
                {
                    this._websocket.Send(JsonConvert.SerializeObject(message));
                    return true;
                }
                catch (Exception)
                {
                    return false;
                }
            }

            return false;
        }

        private void Connect()
        {
            lock (this._sync)
            {
                if (this._isDisposed)
                    return;

                try
                {
                    this.State = ConnectionState.Connecting;

                    this._websocket.Open();
                }
                catch (Exception)
                {
                    this.State = ConnectionState.Disconnected;
                    Reconnect();
                }
            }
        }

        private void Reconnect()
        {
            if (this._isDisposed)
                return;

            this._reconnectTimer.Start();
        }

        private void ReconnectTimerElapsed(object sender, ElapsedEventArgs e)
        {
            Connect();
        }

        private void websocket_Closed(object sender, EventArgs e)
        {
            lock (this._sync)
            {
                if (this._isDisposed)
                    return;

                this.State = ConnectionState.Disconnected;
            }

            EventHandler<EventArgs> events = this.Disconnected;
            if (events != null)
                events.Invoke(this, e);

            Reconnect();
        }

        private void websocket_Error(object sender, ErrorEventArgs e)
        {
            ErrorEventArgs eventArgs = e.Exception is PusherChannelException
                ? e
                : new ErrorEventArgs(new PusherChannelException(e.Exception));

            EventHandler<ErrorEventArgs> events = this.Error;
            if (events != null)
                events.Invoke(this, eventArgs);
        }

        private void websocket_MessageReceived(object sender, MessageReceivedEventArgs e)
        {
            PusherMessage message = JsonConvert.DeserializeObject<PusherMessage>(e.Message);

            switch (message.EventType)
            {
                case "pusher:connection_established":
                    {
                        lock (this._sync)
                        {
                            if (this._isDisposed)
                                return;

                            PusherConnected socketId =
                                JsonConvert.DeserializeObject<PusherConnected>(message.Data.ToString());
                            this._socketId = socketId.SocketId;

                            this.State = ConnectionState.Connected;
                        }

                        EventHandler<EventArgs> events = this.Connected;
                        if (events != null)
                            events.Invoke(this, e);

                        break;
                    }
                case "pusher_internal:subscription_succeeded":
                    {
                        PusherSubscribed subscribed = JsonConvert.DeserializeObject<PusherSubscribed>(message.Data.ToString());

                        EventHandler<SubscribedEventArgs> events = this.Subscribed;
                        if (events != null)
                            events.Invoke(this, new SubscribedEventArgs(subscribed.Channel));

                        break;
                    }
                case "pusher:error":
                    {
                        PusherError error = JsonConvert.DeserializeObject<PusherError>(message.Data.ToString());
                        if (error.Code >= 4000 && error.Code <= 4099)
                            this.State = ConnectionState.Failed;
                            this.Dispose();

                        break;
                    }
                case "pusher:ping":
                    {
                        this.Send(new PusherMessage("pong", null));
                        break;
                    }
                default:
                    {
                        PusherEventMessage eventMessage = JsonConvert.DeserializeObject<PusherEventMessage>(e.Message);

                        EventHandler<MessageEventArgs> events = this.MessageReceived;
                        if (events != null)
                            events.Invoke(this, new MessageEventArgs(eventMessage));

                        break;
                    }
            }
        }

        public void Dispose()
        {
            lock (this._sync)
            {
                this._isDisposed = true;

                this._reconnectTimer.Stop();

                if (this.State != ConnectionState.Failed)
                    this.State = ConnectionState.Disconnected;

                this._websocket.Close();
            }

            EventHandler<EventArgs> events = this.Disconnected;
            if (events != null)
                events.Invoke(this, new EventArgs());
        }
    }
}
